package nl.utwente.ewi.hmi.multitouch.drivers.dtproxy;

import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import com.merl.diamondtouch.DtlibSegment;

import nl.utwente.ewi.hmi.multitouch.SimpleTangible;
import nl.utwente.ewi.hmi.multitouch.SurfaceType;
import nl.utwente.ewi.hmi.multitouch.Tangible;
import nl.utwente.ewi.hmi.multitouch.analysis.Segment;
import nl.utwente.ewi.hmi.multitouch.io.TouchStream;

public class DTProxyStream extends TouchStream {

	private final int port;
	private final InetAddress host;

	private final int userCount;

	private final Tangible[] tangibles;

	private final Set<Object> actors;
	
	private DatagramSocket socket = null;

	private final int[] xSegments;
	private final int[] ySegments;
	private final List<DiamondTouchSegment>[] cachedXSegments;
	private final List<DiamondTouchSegment>[] cachedYSegments;

	private final int[] holdFrame;

	private final byte[] backingBuffer;
	private final ByteBuffer buffer;

	public DTProxyStream(DiamondTouchProxyDeviceInfo touchDeviceInfo) {
		super(touchDeviceInfo);

		this.port = touchDeviceInfo.getPort();
		this.host = touchDeviceInfo.getHost();
		this.userCount = touchDeviceInfo.getUserCount();

		this.xSegments = new int[userCount];
		this.ySegments = new int[userCount];
		this.cachedXSegments = new List[userCount];
		this.cachedYSegments = new List[userCount];

		this.holdFrame = new int[userCount];
		
		this.tangibles = new Tangible[userCount];

		Set<Object> actors = new HashSet<Object>();

		for(int i = 0; i < userCount; i++) {
			final int c = i;
			final Object owner = new Object() {
				@Override
				public String toString() {
					return "DiamondTouch user " + c;
				}
			};
			actors.add(owner);
			
			this.tangibles[i] = new SimpleTangible(owner, SurfaceType.UNKNOWN);
		}

		this.actors = Collections.unmodifiableSet(actors);

		
		this.backingBuffer = new byte[65536];

		this.buffer = ByteBuffer.wrap(backingBuffer);
	}
	
	@Override
	public synchronized boolean close() throws IOException {
		if(socket == null) {
			return false;
		}

		try {
			socket.close();
		} finally {
			socket = null;
		}

		return true;
	}

	@Override
	public Set<Object> getActors() {
		return this.actors;
	}

	public synchronized void read(Set<Segment> segments) throws IOException {
		
		if(socket == null) {
			return;
		}

		DatagramPacket packet = new DatagramPacket(this.backingBuffer, this.backingBuffer.length);
		socket.receive(packet);
		
		this.buffer.rewind();
		
		DiamondTouchPacket dtPacket = DiamondTouchPacket.fromByteBuffer(buffer);

		int frameIndex = 0;

		for(DiamondTouchFrame frame : dtPacket.frames) {
			long time = frame.timeStamp;
			Tangible tangible = tangibles[frameIndex];

			List<DiamondTouchSegment> xSegments = frame.xSegments;
			List<DiamondTouchSegment> ySegments = frame.ySegments;

			final int previousXSegmentCount = this.xSegments[frameIndex];
			final int previousYSegmentCount = this.ySegments[frameIndex];

			this.xSegments[frameIndex] = xSegments.size();
			this.ySegments[frameIndex] = ySegments.size();

			boolean useCache = false;

			// If only the amount of horizontal frames was increased by one or
			// only the amount of vertical frames was increased by one, wait
			// for at most one frame... then continue operation.
			if(holdFrame[frameIndex] > 0) {
				holdFrame[frameIndex]--;
				useCache = holdFrame[frameIndex] > 0;
			} else if((xSegments.size() > previousXSegmentCount) ^ (ySegments.size() > previousYSegmentCount)) {
				this.holdFrame[frameIndex] = 4;
				useCache = true;
			} else if((xSegments.size() < previousXSegmentCount) ^ (ySegments.size() < previousYSegmentCount)) {
				this.holdFrame[frameIndex] = 4;
				useCache = true;
			}
			
			if(useCache && this.cachedXSegments[frameIndex] != null) {
				xSegments = this.cachedXSegments[frameIndex];
				ySegments = this.cachedYSegments[frameIndex];
			} else {
				this.cachedXSegments[frameIndex] = xSegments;
				this.cachedYSegments[frameIndex] = ySegments;
			}
			
			for(DiamondTouchSegment xSegment : xSegments) {
				for(DiamondTouchSegment ySegment : ySegments) {
					segments.add(new DTSegment(xSegment, ySegment, time, tangible));
				}	
			}
			frameIndex++;
		}

	}
	
	@Override
	public synchronized boolean isOpen() {
		return this.socket != null;
	}

	@Override
	public synchronized boolean open() throws IOException {
		if(socket != null) {
			return false;
		}

		if(host.isMulticastAddress()) {
			MulticastSocket multicastSocket = new MulticastSocket(this.port);
			multicastSocket.joinGroup(this.host);

			socket = multicastSocket;
		} else {
			socket = new DatagramSocket(this.port, this.host);
		}

		return true;
	}

	private static final class DTSegment implements Segment {

		final Rectangle2D bounds;
		final float mass;
		final Point2D origin;
		final Path2D perimeter;
		final Tangible tangible;
		final long time;

		public DTSegment(DiamondTouchSegment xSegment, DiamondTouchSegment ySegment, long time, Tangible tangible) {
			int width = xSegment.endPos - xSegment.startPos;
			int height = ySegment.endPos - ySegment.startPos;

			this.bounds = new Rectangle2D.Float(xSegment.startPos, ySegment.startPos, width, height);
			this.perimeter = new Path2D.Float(this.bounds);
			this.origin = new Point2D.Float((float)this.bounds.getCenterX(), (float)this.bounds.getCenterY());

			this.time = time;
			this.tangible = tangible;
			this.mass = width * height;
		}

		@Override
		public Rectangle2D getBounds() {
			return this.bounds;
		}

		@Override
		public float getMass() {
			return this.mass;
		}

		@Override
		public Point2D getOrigin() {
			return this.origin;
		}

		@Override
		public Path2D getPerimeter() {
			return this.perimeter;
		}

		@Override
		public Tangible getTangible() {
			return this.tangible;
		}

		@Override
		public long getTime() {
			return this.time;
		}

		@Override
		public boolean isAmbiguous() {
			return true;
		}

		@Override
		public int compareTo(Segment o) {
			return (int) (o.getTime() - this.getTime());
		}
		
	}
	
}
